diff options
Diffstat (limited to 'app/api/auth/[...nextauth]/saml/provider.ts')
| -rw-r--r-- | app/api/auth/[...nextauth]/saml/provider.ts | 259 |
1 files changed, 259 insertions, 0 deletions
diff --git a/app/api/auth/[...nextauth]/saml/provider.ts b/app/api/auth/[...nextauth]/saml/provider.ts new file mode 100644 index 00000000..8486a690 --- /dev/null +++ b/app/api/auth/[...nextauth]/saml/provider.ts @@ -0,0 +1,259 @@ +import CredentialsProvider from "next-auth/providers/credentials" +import { getOrCreateSAMLUser, validateSAMLUserData } from '@/lib/users/saml-service' +import { encode } from 'next-auth/jwt' +import type { User } from 'next-auth' +import type { SAMLUser } from './utils' +import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils' + +interface SAMLProviderOptions { + id: string + name: string + idp: { + sso_login_url: string + sso_logout_url: string + certificates: string[] + } + sp: { + entity_id: string + private_key: string + certificate: string + assert_endpoint: string + } +} + +export function SAMLProvider(options: SAMLProviderOptions) { + return CredentialsProvider({ + id: options.id, + name: options.name, + credentials: { + user: { + label: "User Data", + type: "text" + } + }, + async authorize(credentials) { + debugLog('๐ SAMLProvider.authorize called with credentials:', credentials); + + try { + debugLog('๐ Checking credentials.user:', { + hasCredentials: !!credentials, + hasUser: !!credentials?.user, + userType: typeof credentials?.user, + userValue: credentials?.user?.substring?.(0, 100) + '...' + }); + + if (!credentials?.user) { + debugError('No user data provided in credentials') + return null + } + + debugProcess('SAML Provider: Processing user data') + + // ์ฌ์ฉ์ ๋ฐ์ดํฐ ํ์ฑ (UTF-8 ์ฒ๋ฆฌ ๊ฐ์ ) + const userDataString = credentials.user + debugLog('๐ค Raw user data string:', userDataString.substring(0, 200) + '...') + + let userData; + try { + userData = JSON.parse(userDataString); + debugSuccess('JSON parsing successful:', userData); + } catch (parseError) { + debugError('JSON parsing failed:', parseError); + debugError('Raw string that failed to parse:', userDataString); + return null; + } + + // ํ์ฑ๋ ๋ฐ์ดํฐ์ UTF-8 ํ์ธ + debugLog('๐ค Parsed user data UTF-8 check:', { + name: userData.name, + nameLength: userData.name?.length, + charCodes: userData.name ? [...userData.name].map(c => c.charCodeAt(0)) : [] + }) + + if (!userData.id || !userData.email) { + debugError('Invalid SAML user data:', userData) + return null + } + + debugSuccess('SAML Provider: User authenticated successfully', { + id: userData.id, + email: userData.email, + name: userData.name + }) + + // ๐ฅ SAML ์ฌ์ฉ์ ๋ฐ์ดํฐ ๊ฒ์ฆ + debugProcess('Validating SAML user data structure...'); + const isValidData = await validateSAMLUserData(userData) + debugLog('Validation result:', isValidData); + if (!isValidData) { + debugError('Invalid SAML user data structure:', userData) + return null + } + + // ๐ฅ JIT (Just-In-Time) ์ฌ์ฉ์ ์์ฑ ๋๋ ์กฐํ + debugProcess('Creating/getting SAML user from database...'); + const userCreateData = { + email: userData.email, + name: userData.name, + companyId: undefined, + techCompanyId: undefined, + domain: userData.domain + }; + debugLog('User create data:', userCreateData); + + const dbUser = await getOrCreateSAMLUser(userCreateData) + debugLog('Database user result:', dbUser); + + if (!dbUser) { + debugError('Failed to get or create SAML user') + return null + } + + // DB์์ ๊ฐ์ ธ์จ ์ค์ ์ฌ์ฉ์ ์ ๋ณด ๋ฐํ + const userResult = { + id: String(dbUser.id), // DB์ ์ค์ ID + name: dbUser.name, // DB์ ์ค์ ์ด๋ฆ + email: dbUser.email, // DB์ ์ค์ ์ด๋ฉ์ผ + companyId: dbUser.companyId, // DB์ ์ค์ ํ์ฌ ID + techCompanyId: dbUser.techCompanyId, // DB์ ์ค์ ๊ธฐ์ ํ์ฌ ID + domain: dbUser.domain, // DB์ ์ค์ ๋๋ฉ์ธ + imageUrl: dbUser.imageUrl, // DB์ ์ค์ ์ด๋ฏธ์ง URL + } + + debugSuccess('SAML Provider: Returning user data to NextAuth:', userResult) + return userResult + } catch (error) { + debugError('SAML Provider: Authentication failed', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + errorType: typeof error, + credentials: credentials + }); + return null + } + } + }) +} + +// SAML ๋ก๊ทธ์ธ URL ์์ฑ ํฌํผ ํจ์ +export function getSAMLLoginUrl(options: SAMLProviderOptions): string { + const params = new URLSearchParams({ + SAMLRequest: 'placeholder', // ์ค์ ๋ก๋ createAuthnRequest()๋ก ์์ฑ + RelayState: options.sp.assert_endpoint, + }) + + return `${options.idp.sso_login_url}?${params.toString()}` +} + +// SAML ์ค์ ๊ฒ์ฆ +export function validateSAMLOptions(options: SAMLProviderOptions): boolean { + const required = [ + options.idp.sso_login_url, + options.sp.entity_id, + options.sp.assert_endpoint + ] + + return required.every(field => field && field.length > 0) +} + +// SAMLProvider์ authorize ํจ์๋ฅผ ์ง์ ํธ์ถํ๊ธฐ ์ํ ํฌํผ +export async function authenticateSAMLUser(userData: SAMLUser) { + debugLog('authenticateSAMLUser called with:', userData); + + try { + // SAMLProvider ๋์ ์ง์ ๋ก์ง ์คํ (Provider ๋ํผ ์์ด) + debugProcess('SAML User Authentication: Processing user data') + + // ์ฌ์ฉ์ ๋ฐ์ดํฐ ๊ฒ์ฆ + if (!userData.id || !userData.email) { + debugError('Invalid SAML user data:', userData) + return null + } + + debugSuccess('SAML User data validated successfully', { + id: userData.id, + email: userData.email, + name: userData.name + }) + + // ๐ฅ SAML ์ฌ์ฉ์ ๋ฐ์ดํฐ ๊ฒ์ฆ + debugLog('Validating SAML user data structure...'); + const isValidData = await validateSAMLUserData(userData) + debugLog('Validation result:', isValidData); + if (!isValidData) { + debugError('Invalid SAML user data structure:', userData) + return null + } + + // ๐ฅ JIT (Just-In-Time) ์ฌ์ฉ์ ์์ฑ ๋๋ ์กฐํ + debugLog('Creating/getting SAML user from database...'); + const userCreateData = { + email: userData.email, + name: userData.name, + companyId: undefined, + techCompanyId: undefined, + domain: userData.domain + }; + debugLog('User create data:', userCreateData); + + const dbUser = await getOrCreateSAMLUser(userCreateData) + debugLog('Database user result:', dbUser); + + if (!dbUser) { + debugError('Failed to get or create SAML user') + return null + } + + // DB์์ ๊ฐ์ ธ์จ ์ค์ ์ฌ์ฉ์ ์ ๋ณด ๋ฐํ + const userResult = { + id: String(dbUser.id), // DB์ ์ค์ ID + name: dbUser.name, // DB์ ์ค์ ์ด๋ฆ + email: dbUser.email, // DB์ ์ค์ ์ด๋ฉ์ผ + companyId: dbUser.companyId, // DB์ ์ค์ ํ์ฌ ID + techCompanyId: dbUser.techCompanyId, // DB์ ์ค์ ๊ธฐ์ ํ์ฌ ID + domain: dbUser.domain, // DB์ ์ค์ ๋๋ฉ์ธ + imageUrl: dbUser.imageUrl, // DB์ ์ค์ ์ด๋ฏธ์ง URL + } + + debugSuccess('SAML User Authentication completed:', userResult) + return userResult; + + } catch (error) { + debugError('authenticateSAMLUser error:', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + userData + }); + return null; + } +} + +// NextAuth JWT ํ ํฐ ์์ฑ ํฌํผ +export async function createNextAuthToken(user: User): Promise<string> { + const token = { + id: user.id, + email: user.email, + name: user.name, + companyId: user.companyId, + techCompanyId: user.techCompanyId, + domain: user.domain, + imageUrl: user.imageUrl, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30์ผ + }; + + const secret = process.env.NEXTAUTH_SECRET!; + return await encode({ token, secret }); +} + +// NextAuth ์ธ์
์ฟ ํค ์ด๋ฆ ๊ฐ์ ธ์ค๊ธฐ +export function getSessionCookieName(): string { + // NEXTAUTH_URL์ด HTTPS์ธ ๊ฒฝ์ฐ์๋ง __Secure- ์ ๋์ฌ ์ฌ์ฉ + const nextAuthUrl = process.env.NEXTAUTH_URL || ''; + const isHttps = nextAuthUrl.startsWith('https://'); + + return isHttps + ? '__Secure-next-auth.session-token' + : 'next-auth.session-token'; +} +
\ No newline at end of file |
